﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using WcfFlashRemoting.AmfObjects;

namespace WcfFlashRemoting.IO.Readers
{
    internal class Amf3Reader : Amf0Reader
    {
        List<Object> _objectReferences = new List<Object>(15);
        List<string> _stringReferences = new List<string>(15);
        List<ClassDefinition> _classDefinitions = new List<ClassDefinition>(2);

        public Amf3Reader(BinaryReader reader)
            : base(reader)
        {
        }

        protected override void Reset()
        {
            base.Reset();
            _objectReferences.Clear();
            _stringReferences.Clear();
            _classDefinitions.Clear();
        }

        protected override object ReadAmfContent(Amf0TypeCode typeCode)
        {
            if (typeCode == Amf0TypeCode.Amf3Tag)
            {
                return ReadAmf3Content();
            }
            return base.ReadAmfContent(typeCode);
        }

        private object ReadAmf3Content()
        {
            Amf3TypeCode typeCode = (Amf3TypeCode)ReadAmfByte();
            switch (typeCode)
            {
                case Amf3TypeCode.Undefined:
                    return AmfUndefined.Instance;
                case Amf3TypeCode.Null:
                    return null;
                case Amf3TypeCode.BooleanFalse:
                    return false;
                case Amf3TypeCode.BooleanTrue:
                    return true;
                case Amf3TypeCode.Integer:
                    return ReadAmf3Int();
                case Amf3TypeCode.Number:
                    return reader.ReadDouble();
                case Amf3TypeCode.String:
                    return ReadAmf3String();
                case Amf3TypeCode.DateTime:
                    return ReadAmf3ObjectOrReference(ReadAmf3DateTime);
                case Amf3TypeCode.Array:
                    return ReadAmf3ObjectOrReference(ReadAmf3Array);
                case Amf3TypeCode.Object:
                    return ReadAmf3ObjectOrReference(ReadAmf3Object);
                case Amf3TypeCode.Xml:
                case Amf3TypeCode.Xml2:
                    return ReadAmf3ObjectOrReference(ReadAmf3XmlDocument);
                case Amf3TypeCode.ByteArray:
                    return ReadAmf3ObjectOrReference(ReadAmf3ByteArray);
                default:
                    throw new InvalidOperationException();
            }
        }

        #region Object References

        delegate object ObjectLoader(int var);
        private object ReadAmf3ObjectOrReference(ObjectLoader loader)
        {
            int temp = ReadAmf3Int();
            bool inline = ((temp & 1) != 0);
            temp = temp >> 1;
            if (inline)
            {
                int arraySize = temp;
                object result = loader(arraySize);
                return result;
            }
            else
            {
                int index = temp;
                return ReadAmf3Reference(index);
            }
        }

        private void AddAmf3ObjectReference(object instance)
        {
            _objectReferences.Add(instance);
        }

        private object ReadAmf3Reference(int handle)
        {
            return _objectReferences[handle];
        }

        #endregion

        private object ReadAmf3Array(int arraySize)
        {
            string key = ReadAmf3String();
            if (string.IsNullOrEmpty(key))
            {
                // indexed array
                object[] array = new object[arraySize];
                AddAmf3ObjectReference(array);
                for (int i = 0; i < arraySize; i++)
                {
                    //Grab the type for each element.
                    object value = ReadAmf3Content();
                    array[i] = value;
                }
                return array;
            }
            else
            {
                Dictionary<string, object> hashtable = new Dictionary<string, object>();
                AddAmf3ObjectReference(hashtable);
                // associative array
                while (!string.IsNullOrEmpty(key))
                {
                    object value = ReadAmf3Content();
                    hashtable.Add(key, value);
                    key = ReadAmf3String();
                }

                for (int i = 0; i < arraySize; i++)
                {
                    object value = ReadAmf3Content();
                    hashtable.Add(i.ToString(), value);
                }
                return hashtable;
            }
        }

        private object ReadAmf3DateTime(int unused)
        {
            double milliseconds = reader.ReadDouble();
            DateTime start = new DateTime(1970, 1, 1, 0, 0, 0);

            DateTime date = start.AddMilliseconds(milliseconds);
            date = DateTime.SpecifyKind(date, DateTimeKind.Utc); // AMF3 always sends UTC
            date = date.ToLocalTime(); // convert it to local timezone
            AddAmf3ObjectReference(date);
            return date;
        }

        private object ReadAmf3Object(int classHandle)
        {
            ClassDefinition classDefinition = ReadAmfClassDefinition(classHandle);
            return ReadAmf3Object(classDefinition);
        }

        private ClassDefinition ReadAmfClassDefinition(int classHandle)
        {
            ClassDefinition classDefinition = null;
            //an inline object
            bool inline = ((classHandle & 1) != 0);
            classHandle = classHandle >> 1;
            if (inline)
            {
                //inline class-def
                string typeIdentifier = ReadAmf3String();
                //flags that identify the way the object is serialized/deserialized
                bool externalizable = ((classHandle & 1) != 0);
                classHandle = classHandle >> 1;
                bool dynamic = ((classHandle & 1) != 0);
                classHandle = classHandle >> 1;

                ClassMember[] members = new ClassMember[classHandle];
                for (int i = 0; i < classHandle; i++)
                {
                    string name = ReadAmf3String();
                    ClassMember classMember = new ClassMember(name, System.Reflection.BindingFlags.Default, System.Reflection.MemberTypes.Custom);
                    members[i] = classMember;
                }
                classDefinition = new ClassDefinition(typeIdentifier, members, externalizable, dynamic);
                _classDefinitions.Add(classDefinition);
            }
            else
            {
                classDefinition = _classDefinitions[classHandle];
            }
            return classDefinition;
        }

        private object ReadAmf3Object(ClassDefinition classDefinition)
        {
            NamedDictionary dict = new NamedDictionary();
            dict.TypeName = classDefinition.ClassName;
            AddAmf3ObjectReference(dict);

            if (classDefinition.Externalizable)
            {
                throw new NotSupportedException();
            }
            else
            {
                for (int i = 0; i < classDefinition.Members.Length; i++)
                {
                    string key = classDefinition.Members[i].Name;
                    object value = ReadAmf3Content();
                    dict.Properties[key] = value;
                }

                if (classDefinition.Dynamic)
                {
                    string key = ReadAmf3String();
                    while (!string.IsNullOrEmpty(key))
                    {
                        object value = ReadAmf3Content();
                        dict.Properties[key] = value;
                        key = ReadAmf3String();
                    }
                }
            }
            return dict;
        }

        private object ReadAmf3ByteArray(int length)
        {
            byte[] buffer = reader.ReadBytes(length);
            AddAmf3ObjectReference(buffer);
            return buffer;
        }

        private object ReadAmf3XmlDocument(int xmlLength)
        {
            string xml = ReadUTF(xmlLength);
            System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument();
            xmlDocument.LoadXml(xml);
            AddAmf3ObjectReference(xmlDocument);
            return xmlDocument;
        }

        #region Referenced Strings

        private string ReadAmf3String()
        {
            int handle = ReadAmf3Int();
            bool inline = ((handle & 1) != 0);
            handle = handle >> 1;
            if (inline)
            {
                int length = handle;
                if (length == 0)
                    return string.Empty;
                string str = ReadUTF(length);
                AddStringReference(str);
                return str;
            }
            else
            {
                return ReadStringReference(handle);
            }
        }

        private void AddStringReference(string str)
        {
            _stringReferences.Add(str);
        }

        private string ReadStringReference(int handle)
        {
            return _stringReferences[handle];
        }
        #endregion

        /// <summary>
        /// Handle decoding of the variable-length representation which gives seven bits of value per serialized byte by using the high-order bit 
        /// of each byte as a continuation flag.
        /// </summary>
        /// <returns></returns>
        private int ReadAmf3Int()
        {
            int acc = reader.ReadByte();
            int tmp;
            if (acc < 128)
                return acc;
            else
            {
                acc = (acc & 0x7f) << 7;
                tmp = reader.ReadByte();
                if (tmp < 128)
                    acc = acc | tmp;
                else
                {
                    acc = (acc | tmp & 0x7f) << 7;
                    tmp = reader.ReadByte();
                    if (tmp < 128)
                        acc = acc | tmp;
                    else
                    {
                        acc = (acc | tmp & 0x7f) << 8;
                        tmp = reader.ReadByte();
                        acc = acc | tmp;
                    }
                }
            }
            //To sign extend a value from some number of bits to a greater number of bits just copy the sign bit into all the additional bits in the new format.
            //convert/sign extend the 29bit two's complement number to 32 bit
            const int mask = 1 << 28; // mask
            int r = -(acc & mask) | acc;
            return r;

            //The following variation is not portable, but on architectures that employ an 
            //arithmetic right-shift, maintaining the sign, it should be fast. 
            //s = 32 - 29;
            //r = (x << s) >> s;
        }

    }
}
